﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.XPath;

namespace CableBeachMessages
{
    public class XrdParseException : Exception
    {
        public XrdParseException() : base() { }
        public XrdParseException(string msg) : base(msg) { }
        public XrdParseException(string msg, System.Exception inner) : base(msg, inner) { }
    }

    public class XrdUri
    {
        public readonly Uri Uri;
        public readonly int Priority;

        public XrdUri(Uri uri, int priority)
        {
            Uri = uri;
            Priority = priority;
        }

        public XrdUri(Uri uri)
            : this(uri, -1)
        { }
    }

    public class XrdUriTemplate
    {
        public readonly string UriTemplate;
        public readonly int Priority;

        public XrdUriTemplate(string uriTemplate, int priority)
        {
            UriTemplate = uriTemplate;
            Priority = priority;
        }

        public XrdUriTemplate(string uriTemplate)
            : this(uriTemplate, -1)
        { }
    }

    public class XrdLink
    {
        public readonly Uri Relation;
        public readonly string MediaType;
        public readonly List<XrdUri> Uris;
        public readonly List<XrdUriTemplate> UriTemplates;

        public XrdLink(Uri relation, string mediaType, List<XrdUri> uris, List<XrdUriTemplate> uriTemplates)
        {
            Relation = relation;
            MediaType = mediaType;
            Uris = uris;
            UriTemplates = uriTemplates;
        }

        public XrdLink(Uri relation, string mediaType, XrdUri uri)
            : this(relation, mediaType, new List<XrdUri> { uri }, new List<XrdUriTemplate>(0))
        { }
    }

    public class XrdDocument
    {
        public readonly string Subject;
        public readonly string Expires; // TODO: Should be a DateTime
        public readonly List<string> Aliases;
        public readonly List<string> Types;
        public readonly List<XrdLink> Links;

        public XrdDocument(string subject)
        {
            Subject = subject;
            Expires = null;
            Aliases = new List<string>();
            Types = new List<string>();
            Links = new List<XrdLink>();
        }

        public XrdDocument(string subject, string expires)
        {
            Subject = subject;
            Expires = expires;
            Aliases = new List<string>();
            Types = new List<string>();
            Links = new List<XrdLink>();
        }

        public XrdDocument(string subject, string expires, List<string> aliases, List<string> types, List<XrdLink> links)
        {
            Subject = subject;
            Expires = expires;
            Aliases = aliases;
            Types = types;
            Links = links;
        }
    }

    public class XrdParser
    {
        private XPathDocument doc;
        private XPathNavigator cursor;

        private XrdDocument result;
        private bool parsed = false;

        private XPathExpression expiresExp;
        private XPathExpression subjectExp;
        private XPathExpression aliasesExp;
        private XPathExpression typesExp;
        private XPathExpression linkRelExp;
        private XPathExpression linkMediatypeExp;
        private XPathExpression linkUriExp;
        private XPathExpression linkUriTemplateExp;

        public XrdParser(Stream xrd)
        {
            doc = new XPathDocument(xrd);
            result = new XrdDocument(null);
            cursor = doc.CreateNavigator();

            expiresExp = cursor.Compile("/XRD/Expires");
            subjectExp = cursor.Compile("/XRD/Subject");
            aliasesExp = cursor.Compile("/XRD/Alias");
            typesExp = cursor.Compile("/XRD/Type");

            linkRelExp = cursor.Compile("Rel");
            linkMediatypeExp = cursor.Compile("MediaType");
            linkUriExp = cursor.Compile("URI");
            linkUriTemplateExp = cursor.Compile("URITemplate");
        }

        public XrdDocument Document
        {
            get
            {
                if (!parsed) Parse();
                return result;
            }
        }

        public static string WriteXrd(XrdDocument document)
        {
            if (document == null)
                throw new ArgumentNullException("document");

            StringBuilder xrd = new StringBuilder();
            xrd.AppendLine("<XRD>");
            xrd.AppendLine("\t<Subject>" + document.Subject + "</Subject>");

            foreach (string type in document.Types)
                xrd.AppendLine("\t<Type>" + type + "</Type>");

            if (!String.IsNullOrEmpty(document.Expires))
                xrd.AppendLine("<Expires>" + document.Expires + "</Expires>");

            foreach (XrdLink link in document.Links)
            {
                xrd.AppendLine("\t<Link>");
                xrd.AppendLine("\t\t<Rel>" + link.Relation + "</Rel>");

                if (!String.IsNullOrEmpty(link.MediaType))
                    xrd.AppendLine("\t\t<MediaType>" + link.MediaType + "</MediaType>");

                foreach (XrdUri uri in link.Uris)
                {
                    if (uri.Priority >= 0)
                        xrd.AppendLine("\t\t<URI priority=\"" + uri.Priority + "\">" + uri.Uri + "</URI>");
                    else
                        xrd.AppendLine("\t\t<URI>" + uri.Uri + "</URI>");
                }

                foreach (XrdUriTemplate uriTemplate in link.UriTemplates)
                {
                    if (uriTemplate.Priority >= 0)
                        xrd.AppendLine("\t\t<URITemplate priority=\"" + uriTemplate.Priority + "\">" + uriTemplate.UriTemplate + "</URI>");
                    else
                        xrd.AppendLine("\t\t<URI>" + uriTemplate.UriTemplate + "</URI>");
                }

                xrd.AppendLine("\t</Link>");
            }

            xrd.AppendLine("</XRD>");

            return xrd.ToString();
        }

        private void Parse()
        {
            var expires = GetAll(cursor.Select(expiresExp));
            var subject = GetAll(cursor.Select(subjectExp));
            var aliases = GetAll(cursor.Select(aliasesExp));
            var types = GetAll(cursor.Select(typesExp));

            if ((expires.Count > 1) || (subject.Count != 1))
                throw new XrdParseException();

            List<XrdLink> links = new List<XrdLink>();

            XPathNodeIterator linkIter = cursor.Select("/XRD/Link");
            while (linkIter.MoveNext())
            {
                var rel = GetAll(linkIter.Current.Select(linkRelExp));
                var mediatype = GetAll(linkIter.Current.Select(linkMediatypeExp));
                var uri = GetAll(linkIter.Current.Select(linkUriExp));
                var uritemplate = GetAll(linkIter.Current.Select(linkUriTemplateExp));

                if (rel.Count > 1 || mediatype.Count > 1)
                    throw new XrdParseException();

                // FIXME: Handle priority attribute
                List<XrdUri> uris = new List<XrdUri>(uri.Count);
                for (int i = 0; i < uri.Count; i++)
                    uris.Add(new XrdUri(new Uri(uri[i])));

                // FIXME: Handle priority attribute
                List<XrdUriTemplate> uritemplates = new List<XrdUriTemplate>(uritemplate.Count);
                for (int i = 0; i < uritemplate.Count; i++)
                    uritemplates.Add(new XrdUriTemplate(uritemplate[i]));

                links.Add(new XrdLink(new Uri(rel[0]), (mediatype.Count == 1) ? mediatype[0] : null, uris, uritemplates));
            }

            result = new XrdDocument(subject[0], (expires.Count == 1) ? expires[0] : null, aliases, types, links);
        }

        private List<string> GetAll(XPathNodeIterator iter)
        {
            var list = new List<string>(iter.Count);
            while (iter.MoveNext())
                list.Add(iter.Current.Value);
            return list;
        }
    }
}
